# [Java]如何根据需要动态生成Java的class
## 需求描述
现在有一个将excel导入Hive表的需求,导入的大致思路是这样的:
1、使用Java程序将excel文本内容读入内存,使用一个Java bean去接收每一行数据
2、使用spark将java bean转成dataframe
3、将得到的dataframe注册成临时表,使用sql将数据写入hive目标表
由于excel的内容里列数量是不固定的,在第一步中,如果每次都新建一个新的对象去接收,程序的维护未免太复杂,且不具备动态的扩展性,一句话说就是不够优雅,我不允许这样的事情发生!
于是,为了解决这个问题,咨询了同事还有一个类库可以解决这个问题,它就是:
javaassist
## 操作起来!
既然内容是多变的,那么能不能根据实际excel的列数,动态去生成这个类,excel的表头列名就是对象的属性名,列有多少个,对象就有多少个属性!OK,下面邀请本次博客的重磅嘉宾:[javaassist](https://www.javassist.org/tutorial/tutorial.html#pool)
### 构建一个基础对象
maven中引入依赖:
~~~xml
org.javassist
javassist
3.30.2-GA
~~~
下面我们从0开始,构造一个简单的对象,对象包括:
- 无参构造
- 有参构造
- 两个属性
- setter和getter方法
~~~java
public class Person {
private String name;
private String sex;
public void setName(String var1) {
this.name = var1;
}
public String getName() {
return this.name;
}
public void setSex(String var1) {
this.sex = var1;
}
public String getSex() {
return this.sex;
}
public Person(String var1, String var2) {
this.sex = this.sex;
}
public Person() {
}
}
~~~
### 声明类
~~~java
ClassPool classPool = ClassPool.getDefault();
CtClass row = classPool.makeClass("YOUR CLASS NAME");
row.setModifiers(Modifier.PUBLIC);
~~~
使用ClassPool创建了一个PUBLIC类方法,这时候主体已经有了
### 声明私有属性
两个私有属性,分别是name和sex
新建两个Field对象,根据官网,新建一个属性需要new一个CtField对象,此时需要传入属性的类型,但是,默认的只有booleanType、charType、byteType、shortType、intType、longType、floatType、doubleType和voidType,如果需要使用String类型,则需要用`CtClass cc = pool.get("java.lang.String");`来得到
~~~java
/**
* Creates a CtField object.
* The created field must be added to a class
* with CtClass.addField().
* An initial value of the field is specified
* by a CtField.Initializer object.
*
* If getter and setter methods are needed,
* call CtNewMethod.getter() and
* CtNewMethod.setter().
*
* @param type field type
* @param name field name
* @param declaring the class to which the field will be added.
*
* @see CtClass#addField(CtField)
* @see CtNewMethod#getter(String,CtField)
* @see CtNewMethod#setter(String,CtField)
* @see CtField.Initializer
*/
public CtField(CtClass type, String name, CtClass declaring)
throws CannotCompileException
{
this(Descriptor.of(type), name, declaring);
}
~~~
先得到String的CtClass
~~~java
CtClass stringClass = stringClass = classPool.get("java.lang.String");
~~~
再构建属性:
~~~java
//参数依次是属性类型,属性名,累对象的CtClass
CtField a = new CtField(stringClass, colName, row);
a.setModifiers(Modifier.PRIVATE);
~~~
我们用得到类来添加该属性
~~~java
row.addField(a);
~~~
如此往复,再添加另一个属性即可
### 声明getter和setter
~~~java
//setter方法
//setter方法是没有返回值的,所以在此设置为voidType,为了规范命名,将属性名转为驼峰命名格式
//setter方法是有参数的,在此即为属性的类型,为string
CtMethod setMethod = new CtMethod(CtClass.voidType, "set" + Camel.camel(colName), new CtClass[]{stringClass}, row);
setMethod.setModifiers(Modifier.PUBLIC);
setMethod.setBody("this." + colName + "=$1;"); //这里的$1即代表第一个参数
//getter方法
//getter方法是有返回值的,返回值的类型即是属性的类型,在此为string类型,同样属性名转驼峰格式
//getter方法是无参的
CtMethod getMethod = new CtMethod(stringClass, "get" + Camel.camel(colName), new CtClass[]{}, row);
getMethod.setModifiers(Modifier.PUBLIC);
getMethod.setBody("return " + colName + ";");
~~~
在创造的类上添加这两个方法:
~~~java
row.addMethod(setMethod);
row.addMethod(getMethod);
~~~
### 创建构造方法
在此创建一个有两个参数的有参构造和一个无参构造
1、有参构造
~~~java
CtClass[] ctClasses = new CtClass[2];
Arrays.fill(ctClasses, stringClass);
CtConstructor constructor = new CtConstructor(ctClasses, row);
constructor.setModifiers(Modifier.PUBLIC);//有参构造方法是PUBLIC修饰的
constructor.setBody("this." + colName + "= " + colName + ";");
~~~
2、无参构造
~~~java
CtConstructor noArgConstructor = CtNewConstructor.make("public " + cName + "(){}", row);
row.addConstructor(noArgConstructor);
~~~
将以上方法整合起来就得到了一个构建的类,该类是可以在运行时调用的
## 封装
为了后续方便使用,将这些方法封装起来,做成工具使用,下面新建一个工具类,允许传入属性列表,并且默认全部都是string类型,如果需要其他类型,在此基础上修改即可:
~~~java
import com.svw.usually.util.Camel;
import javassist.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
public class ClassMaker {
private ArrayList columns = new ArrayList();
private String className = null;
public ClassMaker(ArrayList columns, String className) {
this.columns = columns;
this.className = className;
}
private static final Logger LOG = LoggerFactory.getLogger(ClassMaker.class);
public Class> makeClass() throws CannotCompileException {
ClassPool classPool = ClassPool.getDefault();
CtClass row = classPool.makeClass(className);
row.setModifiers(Modifier.PUBLIC);
CtClass stringClass = null;
try {
stringClass = classPool.get("java.lang.String");
} catch (NotFoundException e) {
throw new RuntimeException(e);
}
CtClass[] ctClasses = new CtClass[columns.size()];
Arrays.fill(ctClasses, stringClass);
CtConstructor constructor = new CtConstructor(ctClasses, row);
constructor.setModifiers(Modifier.PUBLIC);
for (String colName : columns) {
LOG.info("Adding property name: {}", colName);
CtField a = new CtField(stringClass, colName, row);
a.setModifiers(Modifier.PRIVATE);
row.addField(a);
//set方法
CtMethod setMethod = new CtMethod(CtClass.voidType, "set" + Camel.camel(colName), new CtClass[]{stringClass}, row);
setMethod.setModifiers(Modifier.PUBLIC);
setMethod.setBody("this." + colName + "=$1;");
//get方法
CtMethod getMethod = new CtMethod(stringClass, "get" + Camel.camel(colName), new CtClass[]{}, row);
getMethod.setModifiers(Modifier.PUBLIC);
getMethod.setBody("return " + colName + ";");
row.addMethod(setMethod);
row.addMethod(getMethod);
constructor.setBody("this." + colName + "= " + colName + ";");
}
row.addConstructor(constructor);
String[] pts = className.split(".");
String cName = pts[pts.length - 1];
CtConstructor noArgConstructor = CtNewConstructor.make("public " + cName + "(){}", row);
row.addConstructor(noArgConstructor);
return row.toClass();
}
}
~~~
Camel
~~~java
public class Camel {
public static String camel(String name) {
if (name == null) {
return null;
}
String head = name.substring(0, 1).toUpperCase();
if (name.length() >= 2) {
return head + name.substring(1, name.length());
} else return head;
}
}
~~~
## 后续
以上都只是最简单的用法,由于时间问题,先更新这些,如果后续还花更多时间探索或者解锁了其他玩法,再在这里更新!